package com.opennews.openplatform

import com.opennews.openplatform.constants.*
import com.opennews.openplatform.dto.IncomeExpenseDetailQueryArgument
import grails.compiler.GrailsCompileStatic
import grails.gorm.transactions.Transactional
import groovy.time.TimeCategory
import groovy.transform.CompileDynamic
import org.hibernate.criterion.CriteriaSpecification
import org.hibernate.sql.JoinType

@Transactional(readOnly = true)
@GrailsCompileStatic
class IncomeExpenseDetailService {
    BankService bankService
    IncomeExpenseAnalysisService incomeExpenseAnalysisService
    UserService userService

    /**
     * Queries all income/expense which associated with provided account group.
     * @param queryArgument: The instance of IncomeExpenseDetailQueryArgument which holds criteria. It contains below properties.
     *      pageSize: How many records is return in one trip.
     *      pageIndex: Where to take rows.
     *      accountGroupId: The user's account group id.
     *      keyword: The String which matches IncomeExpenseDetail.details and IncomeExpenseDetail.remark properties as SQL LIKE operation. It will be ignored when value is null or empty.
     *      keywordsSearchCondition: The search condition for multi-keyword. The condition is OR or AND.
     *      type: The String which matches IncomeExpenseDetail.type property. It will be ignored when value is null or empty.
     *      detailType: The String which matches IncomeExpenseDetail.detailType property. It will be ignored when value is null or empty.
     *      paymentType: The String which matches IncomeExpenseDetail.paymentType property. It will be ignored when value is null or empty.
     *      bankCardId: The String which matches IncomeExpenseDetail.bankCard.id property. It will be ignored when value is null or empty.
     *      startDate: IncomeExpenseDetail.incomeExpenseDate property must greater than or equal to this Date value. It will be ignored when value is null.
     *      endDate: IncomeExpenseDetail.incomeExpenseDate property must less than or equal to this Date value. It will be ignored when value is null.
     * @return The list of map instance which contains keys below.
     *          id: String of the income expense detail id.
     *          amount: Float of the income expense detail amount.
     *          type: String of the type. It's income or expense.
     *          detailType: String of the detail type which indicates where the income from or the expense goes.
     *          paymentType: String of the payment type which how the expense payed.
     *          bankId: String of the band id of the bank if the expense is payed by card.
     *          bankShortName: String of the short name of the bank if the expense is payed by card.
     *          bankLogoUrl: String of the logo url of the bank if the expense is payed by card.
     *          bankCardId: String of the id of the bank card if the expense is payed by card.
     *          bankCardName: String of the name of the bank card if the expense is payed by card.
     *          bankCardIsCreditCard: Bool of if this bank card is credit card.
     *          details: String of the income or expense detail.
     */
    @CompileDynamic
    List<Map<String, ?>> queryListByAccountGroup(IncomeExpenseDetailQueryArgument queryArgument) {
        // The start date and end date format is yyyy-MM-dd. No time part. So the default time is 00:00:00.
        // If the start date and end date are both 2019-12-13 00:00:00 and we want to search the data between start and end date
        // we have to change the end date to 2019-12-14 00:00:00.
        // That would satisfy incomeExpenseDate >= startDate && incomeExpenseDate < endDate and match any data with date in 2019-12-13.
        //if (queryArgument.endDate != null) {
        //    use(TimeCategory) {
        //        queryArgument.endDate = queryArgument.endDate + 1.day
        //    }
        //}

        def getSharedDelegate = getQueryByAccountGroupSharedDelegate(queryArgument)

        // Queries detail by account group id.
        return IncomeExpenseDetail.createCriteria().list {
            // Transforms the result data format to Map instance.
            resultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP)

            // Sets the join type for bank card property in order to get its id and name.
            createAlias("bankCard", "bankCard", JoinType.LEFT_OUTER_JOIN)

            // Sets the join type for bank property in order to get its short name.
            createAlias("bankCard.bank", "bank", JoinType.LEFT_OUTER_JOIN)

            // Sets the query condition.
            getSharedDelegate.delegate = delegate
            getSharedDelegate()

            // Specifies the property we need.
            projections {
                property("id", "id")
                property("incomeExpenseDate", "incomeExpenseDate")
                property("amount", "amount")
                property("type", "type")
                property("detailType", "detailType")
                property("paymentType", "paymentType")
                property("bank.id", "bankId")
                property("bank.shortName", "bankShortName")
                property("bankCard.id", "bankCardId")
                property("bankCard.name", "bankCardName")
                property("bankCard.isCreditCard", "bankCardIsCreditCard")
                property("details", "details")
            }

            // Sets the orderby.
            order("incomeExpenseDate", "desc")
            order("type", "desc")
            order("bankCard", "asc")
            order("lastUpdated", "desc")

            // How many rows are return for this trip.
            maxResults(queryArgument.pageSize)

            // Where to take the rows.
            firstResult(queryArgument.pageSize * queryArgument.pageIndex)
        }.each { Map<String, ?> item ->
            // Adds bankLogoUrl key to the map item.
            item.bankLogoUrl = bankService.getLogoUrl(item.bankShortName.toString())
        } as List<Map<String, ?>>
    }

    /**
     * Queries all income/expense total amount which associated with provided account group.
     * @param queryArgument: The instance of IncomeExpenseDetailQueryArgument which holds criteria. It contains below properties.
     *      pageSize: How many records is return in one trip.
     *      pageIndex: Where to take rows.
     *      accountGroupId: The user's account group id.
     *      keyword: The String which matches IncomeExpenseDetail.details and IncomeExpenseDetail.remark properties as SQL LIKE operation. It will be ignored when value is null or empty.
     *      keywordsSearchCondition: The search condition for multi-keyword. The condition is OR or AND.
     *      type: The String which matches IncomeExpenseDetail.type property. It will be ignored when value is null or empty.
     *      detailType: The String which matches IncomeExpenseDetail.detailType property. It will be ignored when value is null or empty.
     *      paymentType: The String which matches IncomeExpenseDetail.paymentType property. It will be ignored when value is null or empty.
     *      bankCardId: The String which matches IncomeExpenseDetail.bankCard.id property. It will be ignored when value is null or empty.
     *      startDate: IncomeExpenseDetail.incomeExpenseDate property must greater than or equal to this Date value. It will be ignored when value is null.
     *      endDate: IncomeExpenseDetail.incomeExpenseDate property must less than or equal to this Date value. It will be ignored when value is null.
     * @return The number of the total amount.
     */
    @CompileDynamic
    BigDecimal queryTotalAmountByAccountGroup(IncomeExpenseDetailQueryArgument queryArgument) {
        //// The start date and end date format is yyyy-MM-dd. No time part. So the default time is 00:00:00.
        //// If the start date and end date are both 2019-12-13 00:00:00 and we want to search the data between start and end date
        //// we have to change the end date to 2019-12-14 00:00:00.
        //// That would satisfy incomeExpenseDate >= startDate && incomeExpenseDate < endDate and match any data with date in 2019-12-13.
        //if (queryArgument.endDate != null) {
        //    use(TimeCategory) {
        //        queryArgument.endDate = queryArgument.endDate + 1.day
        //    }
        //}

        def getSharedDelegate = getQueryByAccountGroupSharedDelegate(queryArgument)

        // Queries sum of amount by account group id.
        def totalAmount = IncomeExpenseDetail.createCriteria().get {
            //// Sets the join type for bank card property in order to get its id and name.
            //createAlias("bankCard", "bankCard", JoinType.LEFT_OUTER_JOIN)
            //
            //// Sets the join type for bank property in order to get its short name.
            //createAlias("bankCard.bank", "bank", JoinType.LEFT_OUTER_JOIN)

            // Sets the query condition.
            getSharedDelegate.delegate = delegate
            getSharedDelegate()

            // Specifies the property we need.
            projections {
                sum("amount")
            }
        } as BigDecimal

        return totalAmount != null ? totalAmount : 0.0
    }

    /**
     * Queries which bank card id is used by income/expense based on provided bank card id list.
     * @param bankCardIds: The string list of bank card id.
     * @param accountGroupId: The string of user accout group id.
     * @return The string list of bank card id which is used by income/expense. Any unused bank card id will be filtered.
     */
    @CompileDynamic
    List<String> queryUsedBankCardIds(List<String> bankCardIds, String accountGroupId) {
        if (bankCardIds.size() > 0) {
            return IncomeExpenseDetail.where {
                bankCard.id in bankCardIds &&
                    accountGroup == AccountGroup.proxy(accountGroupId)
            }.projections {
                distinct("bankCard.id")
            }.toList() as List<String>
        } else {
            return []
        }
    }

    /**
     * Queries which detail type is used by income/expense based on provided income/expense type and detail type name list.
     * @param incomeExpenseType : The string of the income/expense type.
     * @param detailTypeNames: The string list of detail type names.
     * @param accountGroupId: The string of the user's account group id.
     * @return The string list of detail type name which is used by income/expense. Any unused detail type will be filtered.
     */
    @CompileDynamic
    List<String> queryUsedDetailTypeNames(String incomeExpenseType, List<String> detailTypeNames, String accountGroupId) {
        if (detailTypeNames.size() > 0) {
            return IncomeExpenseDetail.where {
                accountGroup == AccountGroup.proxy(accountGroupId) &&
                    type == incomeExpenseType &&
                    detailType in detailTypeNames
            }.projections {
                distinct("detailType")
            }.toList() as List<String>
        } else {
            return []
        }
    }

    /**
     * Queries the income/expense list and day summary for account group by id.
     * @param accountGroupId: The string of the user account group id.
     * @param pageSize: How many records is return in one trip.
     * @param pageIndex: Where to take rows.
     * @return The map instance which contains keys below.
     *          list: The List<Map<String, ?>> return by queryListByAccountGroup. Which contains
     *          an extra item named daySummary which is the List<Map<String, ?>> return by queryDailySummaryList.
     */
    @CompileDynamic
    List<Map<String, ?>> queryListWithSummaryByAccountGroup(String accountGroupId, Integer pageSize, Integer pageIndex) {
        // Prepares the query argument.
        def queryArgument = new IncomeExpenseDetailQueryArgument(accountGroupId: accountGroupId, pageSize: pageSize, pageIndex: pageIndex)

        // Queries the income/expense list by argument.
        def list = queryListByAccountGroup(queryArgument)

        // Gets date list from the detail list.
        List<Date> dates = list.collect { Map<String, ?> p ->
            p.incomeExpenseDate as Date
        }

        // Queries month summary based on the unique date list.
        def monthSummaryList = incomeExpenseAnalysisService.queryMonthlySummaryList(accountGroupId, dates)

        // Goes through the month summary list.
        monthSummaryList.each { Map<String, ?> summary ->
            // Clones the item in order to converting its date format.
            // The original item date property used to match the detail data.
            def summaryWithFormattedDate = summary.clone() as Map<String, ?>

            // Adds monthSummary to the first item of each month.
            list.findAll { Map<String, ?> item ->
                item.incomeExpenseDate.toYearMonth() == summary.incomeExpenseDate.toYearMonth()
            }.first().put('monthSummary', summaryWithFormattedDate)
        }

        // Queries day summary based on the unique date list.
        def daySummaryList = incomeExpenseAnalysisService.queryDailySummaryList(accountGroupId, dates)

        // Goes through the day summary list.
        daySummaryList.each { Map<String, ?> summary ->
            // Clones the item in order to converting its date format.
            // The original item date property used to match the detail data.
            def summaryWithFormattedDate = summary.clone() as Map<String, ?>

            // Adds daySummary to the first item of each date.
            list.findAll { Map<String, ?> item ->
                item.incomeExpenseDate == summary.incomeExpenseDate
            }.first().put('daySummary', summaryWithFormattedDate)
        }

        // Returns result
        return list
    }

    /**
     * Queries income expense details by its id.
     * @param accountGroupId: The string of user account group id.
     * @param id: The string of the income expense detail id.
     * @return The map instance which contains keys below.
     *          id: String of the income expense detail id.
     *          amount: Float of the income expense detail amount.
     *          type: String of the type. It's income or expense.
     *          detailType: String of the detail type which indicates where the income from or the expense goes.
     *          paymentType: String of the payment type which how the expense payed.
     *          bankId: String of the band id of the bank card if the expense is payed by card.
     *          bankShortName: String of the short name of the bank card if the expense is payed by card.
     *          bankCardId: String of the id of the bank card if the expense is payed by card.
     *          bankCardName: String of the name of the bank card if the expense is payed by card.
     *          bankCardEnabled: Bool to indicate if this card is enabled..
     *          details: String of the income or expense detail.
     *          remark: String of the necessary extra remark.
     *          latitude: Float of location latitude.
     *          longitude: Float of location longitude.
     *          address: String of location address.
     *          formattedAddress: String of location formattedAddress.
     *          province: String of location province.
     *          city: String of location city.
     *          district: String of location district.
     *          createdBy: String of username of who created this item.
     *          updatedBy: String of username of who updated this item.
     *          dateCreated: Date of when this detail created.
     *          lastUpdated: Date of when this detail updated.
     */
    @CompileDynamic
    Map<String, ?> queryDetails(String accountGroupId, String id) {
        // Queries detail by account group id.
        Map<String, ?> result = IncomeExpenseDetail.createCriteria().get {
            // Transforms the result data format to Map instance.
            resultTransformer(CriteriaSpecification.ALIAS_TO_ENTITY_MAP)

            // Sets the join type for bank card property in order to get its id and name.
            createAlias("bankCard", "bankCard", JoinType.LEFT_OUTER_JOIN)

            // Sets the join type for bank property in order to get its short name.
            createAlias("bankCard.bank", "bank", JoinType.LEFT_OUTER_JOIN)

            // Sets the query condition.
            and {
                eq("accountGroup.id", accountGroupId)
                eq("id", id)
            }

            // Specifies the property we need.
            projections {
                property("id", "id")
                property("incomeExpenseDate", "incomeExpenseDate")
                property("amount", "amount")
                property("type", "type")
                property("detailType", "detailType")
                property("paymentType", "paymentType")
                property("bank.id", "bankId")
                property("bank.shortName", "bankShortName")
                property("bankCard.id", "bankCardId")
                property("bankCard.name", "bankCardName")
                property("bankCard.enabled", "bankCardEnabled")
                property("details", "details")
                property("remark", "remark")
                property("latitude", "latitude")
                property("longitude", "longitude")
                property("address", "address")
                property("formattedAddress", "formattedAddress")
                property("province", "province")
                property("city", "city")
                property("district", "district")
                property("createdBy", "createdBy")
                property("updatedBy", "updatedBy")
                property("dateCreated", "dateCreated")
                property("lastUpdated", "lastUpdated")
            }
        } as Map<String, ?>

        if (result == null) {
            return [:]
        }

        // Queries createdBy & updatedBy basic info.
        def userDetails = userService.queryBasicInfo(accountGroupId, [result.createdBy, result.updatedBy])
        result.createdBy = userDetails.find { it.username == result.createdBy }
        result.updatedBy = userDetails.find { it.username == result.updatedBy }

        // Sets the bank logo url based on its short name.
        result.put("bankLogoUrl", bankService.getLogoUrl(result.bankShortName.toString()))

        return result
    }

    /**
     *
     * Queries the specific credit card bill based on bank card id, bill year and bill month.
     *
     * We don't generate the bill at this moment by using CreditCardBill and CreditCardBillExpenseDetail because that
     * would introduce more complex logic and effort.
     *
     * @param accountGroupId: The string of the user account group id.
     * @param pageSize: How many records is return in one trip.
     * @param pageIndex: Where to take rows.
     * @param bankCardId: The String of bank card id.
     * @param year: The int of the year of bill.
     * @param month: The int of the month of bill.
     * @return The return result of queryListByAccountGroup method.
     */
    List<Map<String, ?>> queryCreditCardBill(String accountGroupId, Integer pageSize, Integer pageIndex, String bankCardId, Date statementDate) {
        // Queries the statement date of the credit card with its id, statement year and month.
        def cardStatementDateRange = createCreditCardBillStatementDateRange(statementDate)

        // Prepares the query argument.
        def queryArgument = new IncomeExpenseDetailQueryArgument(accountGroupId: accountGroupId, bankCardId: bankCardId, pageSize: pageSize, pageIndex: pageIndex,
            startDate: cardStatementDateRange.statementStartDate, endDate: cardStatementDateRange.statementEndDate)

        // Queries the list which matches the date and bank card.
        return queryListByAccountGroup(queryArgument)
    }

    /**
     * Queries the credit card bill total amount.
     * @param accountGroupId: The string of the user account group id.
     * @param bankCardId: The string of bank card id.
     * @param year: The Integer of year.
     * @param month: The Integer of month.
     * @return The BigDecimal of the total amount.
     */
    BigDecimal queryCreditCardBillAmount(String accountGroupId, String bankCardId, Date statementDate) {
        // Queries the statement date of the credit card with its id, statement year and month.
        def cardStatementDateRange = createCreditCardBillStatementDateRange(statementDate)

        // Prepares the query argument.
        def queryArgument = new IncomeExpenseDetailQueryArgument(accountGroupId: accountGroupId, startDate: cardStatementDateRange.statementStartDate, endDate: cardStatementDateRange.statementEndDate, bankCardId: bankCardId)

        // Queries the total amount within the card statement date range.
        return queryTotalAmountByAccountGroup(queryArgument)
    }

    /**
     * Queries basic statistics including totalIncomeExpenseCount and totalIncomeExpenseDays.
     * @param accountGroupId: The string of the user account group id.
     * @return The map instance which contains keys below.
     *          totalIncomeExpenseCount: Int of total income/expense recorded.
     *          totalIncomeExpenseDays: Int of total income/expense days recorded.
     *          totalUserCount: Int of total account group users.
     */
    @CompileDynamic
    Map<String, ?> queryBasicStatistics(String accountGroupId) {
        def totalUserCount = AccountGroupUser.countByAccountGroup(AccountGroup.proxy(accountGroupId))
        def totalBankCardCount = BankCard.countByAccountGroup(AccountGroup.proxy(accountGroupId))
        def totalIncomeExpenseCount = IncomeExpenseDetail.countByAccountGroup(AccountGroup.proxy(accountGroupId))

        def totalIncomeExpenseDays = IncomeExpenseDetail.createCriteria().get {
            eq("accountGroup.id", accountGroupId)

            projections {
                countDistinct("incomeExpenseDate")
            }
        }

        return [
            totalUserCount: totalUserCount,
            totalBankCardCount: totalBankCardCount,
            totalIncomeExpenseCount: totalIncomeExpenseCount,
            totalIncomeExpenseDays: totalIncomeExpenseDays
        ]
    }

    @CompileDynamic
    List<Map<String, ?>> queryCoordinates(String accountGroupId, String year) {
        // Locations contain same formatted address but different coordinates which we like to take only one match.
        // https://www.navicat.com/en/company/aboutus/blog/1647-applying-select-distinct-to-one-column-only
        def hql = """
                            SELECT
                                formattedAddress,
                                latitude,
                                longitude
                            FROM
                                IncomeExpenseDetail
                            WHERE
                                id IN (
                                    SELECT
                                        MAX(id)
                                    FROM
                                        IncomeExpenseDetail
                                    WHERE
                                        accountGroup.id = :accountGroupId AND
                                        type = :type AND
                                        latitude IS NOT NULL AND
                                        longitude IS NOT NULL AND
                                        formattedAddress IS NOT NULL AND
                                        formattedAddress <> '' AND
                                        (
                                            :year IS NULL OR 
                                            :year = '' OR 
                                            YEAR(incomeExpenseDate) = :year
                                        )
                                    GROUP BY
                                        formattedAddress
                                )
                        """

        return IncomeExpenseDetail.executeQuery(hql, [
            type: IncomeExpenseTypeConstant.EXPENSE,
            accountGroupId: accountGroupId,
            year: year
        ]).collect {[
            formattedAddress: it[0],
            latitude: it[1],
            longitude: it[2]
        ]} as List<Map<String, ?>>
    }

    /**
     * Adds a new record of IncomeExpenseDetail with posted data.
     * The username used to query the account group id and saved to createdBy and updatedBy.
     * @param requestBodyData: The map instance which contains the keys with same names as IncomeExpenseDetail properties.
     * @param accountGroupId: The string of user account group id.
     * @param username: The string of username which is saved to createdBy and updatedBy.
     */
    @Transactional()
    void add(Map<String, ?> requestBodyData, String accountGroupId, String username) {
        // Creates new IncomeExpenseDetail.
        def incomeExpenseDetail = new IncomeExpenseDetail()

        // Updates its properties with posted data.
        updateProperties(incomeExpenseDetail, requestBodyData, username)

        // Associates with account group.
        incomeExpenseDetail.accountGroup = AccountGroup.proxy(accountGroupId)

        // Specifies creator.
        incomeExpenseDetail.createdBy = username

        // Saves change.
        incomeExpenseDetail.save()
    }

    /**
     * Updates existing IncomeExpenseDetail with posted data.
     * @param accountGroupId: The string of user account group id.
     * @param username: The string of username which saved to updatedBy.
     * @param requestBodyData: The map instance which contains the keys with same names as IncomeExpenseDetail properties.
     */
    @Transactional()
    void update(String accountGroupId, String username, Map<String, ?> requestBodyData) {
        // Gets IncomeExpenseDetail by user account group and id.
        def incomeExpenseDetail = IncomeExpenseDetail.findByAccountGroupAndId(AccountGroup.proxy(accountGroupId), requestBodyData.id.toString())

        // Applies parallel authentication.
        if (incomeExpenseDetail != null) {
            // Updates its properties with posted data.
            updateProperties(incomeExpenseDetail, requestBodyData, username)

            // Saves change.
            incomeExpenseDetail.save()
        }
    }

    /**
     * Deletes the IncomeExpenseDetails with provides list of id.
     * @param accountGroupId: The string of user account group id.
     * @param ids The string list of id.
     */
    @Transactional()
    @CompileDynamic
    void delete(String accountGroupId, List<String> ids) {
        if (ids.size() > 0) {
            // Executes batch deleting by DetachedCriteria.
            IncomeExpenseDetail.where {
                id in ids &&
                    accountGroup == AccountGroup.proxy(accountGroupId)
            }.deleteAll()
        }
    }

    /**
     * Updates input IncomeExpenseDetail properties.
     * @param incomeExpenseDetail: The instance of IncomeExpenseDetail which properties will be updated.
     * @param requestBodyData: The map instance which contains the keys with same names as IncomeExpenseDetail properties.
     * @param username: The string of username which saved to updatedBy.
     */
    private void updateProperties(IncomeExpenseDetail incomeExpenseDetail, Map<String, ?> requestBodyData, String username) {
        // Stores the posted bank card data.
        BankCard bankCard

        // Gets the bank card proxy if needed.
        if (requestBodyData.bankCard != null && (requestBodyData.bankCard as Map<String, ?>).id != null) {
            def bankCardData = requestBodyData.bankCard as Map<String, ?>
            bankCard = BankCard.proxy(bankCardData.id.toString())

            // If we don't remove this key we cannot use the constructor to assign value.
            // new IncomeExpenseDetail(requestBodyData) will cause error since it creates a new bank card with it.
            // But properties.putAll() can live with it.
            // Leaving this line of code just in case.
            requestBodyData.remove("bankCard")
        }

        // Updates the bank card property.
        incomeExpenseDetail.bankCard = bankCard

        // Updates all properties which requestBodyData contains.
        incomeExpenseDetail.properties.putAll(requestBodyData)

        // Specifies updater.
        incomeExpenseDetail.updatedBy = username

        // Sets the default payment type if needed.
        if (SharedUtil.isNullOrEmpty(incomeExpenseDetail.paymentType)) {
            incomeExpenseDetail.paymentType = PaymentTypeConstant.CASH
        }

        // Just in case user inputs negative number for income and positive number for expense.
        // We take the absolute value first.
        incomeExpenseDetail.amount = incomeExpenseDetail.amount.abs()

        // If it is expense we take it as negative number.
        if (incomeExpenseDetail.type == IncomeExpenseTypeConstant.EXPENSE) {
            incomeExpenseDetail.amount = -incomeExpenseDetail.amount
        }
    }

    /**
     * Creates the shared delegate closure for criteria usage. Especially for queryListByAccountGroup and queryTotalAmountByAccountGroup methods.
     * @param queryArgument: The instance of IncomeExpenseDetailQueryArgument which holds criteria. It contains below properties.
     *      pageSize: How many records is return in one trip.
     *      pageIndex: Where to take rows.
     *      accountGroupId: The user's account group id.
     *      keyword: The String which matches IncomeExpenseDetail.details, IncomeExpenseDetail.remark and IncomeExpenseDetail.formattedAddress properties as SQL LIKE operation. It will be ignored when value is null or empty.
     *      keywordsSearchCondition: The search condition for multi-keyword. The condition is OR or AND.
     *      type: The String which matches IncomeExpenseDetail.type property. It will be ignored when value is null or empty.
     *      detailType: The String which matches IncomeExpenseDetail.detailType property. It will be ignored when value is null or empty.
     *      paymentType: The String which matches IncomeExpenseDetail.paymentType property. It will be ignored when value is null or empty.
     *      bankCardId: The String which matches IncomeExpenseDetail.bankCard.id property. It will be ignored when value is null or empty.
     *      startDate: IncomeExpenseDetail.incomeExpenseDate property must greater than or equal to this Date value. It will be ignored when value is null.
     *      endDate: IncomeExpenseDetail.incomeExpenseDate property must less than or equal to this Date value. It will be ignored when value is null.
     * @return
     */
    @CompileDynamic
    private Closure getQueryByAccountGroupSharedDelegate(IncomeExpenseDetailQueryArgument queryArgument) {
        // The start date and end date format is yyyy-MM-dd. No time part. So the default time is 00:00:00.
        // If the start date and end date are both 2019-12-13 00:00:00 and we want to search the data between start and end date
        // we have to change the end date to 2019-12-14 00:00:00.
        // That would satisfy incomeExpenseDate >= startDate && incomeExpenseDate < endDate and match any data with date in 2019-12-13.
        if (queryArgument.endDate != null) {
            use(TimeCategory) {
                queryArgument.endDate = queryArgument.endDate + 1.day
            }
        }

        return {
            and {
                // Where a property equals a particular value.
                eq("accountGroup.id", queryArgument.accountGroupId)

                if (queryArgument.startDate != null) {
                    // Where a property is greater than or equal to a particular value.
                    ge("incomeExpenseDate", queryArgument.startDate)
                }

                if (queryArgument.endDate != null) {
                    // Where a property is less than a particular value.
                    lt("incomeExpenseDate", queryArgument.endDate)
                }

                if (!SharedUtil.isNullOrEmpty(queryArgument.keyword)) {
                    if (queryArgument.keywordsSearchCondition == IncomeExpenseDetailQueryArgument.KEYWORDS_SEARCH_AND_CONDITION) {
                        // If user wants to search with AND condition for multiple keywords then we just need to wrap each keyword into one OR section.
                        // The keyword supports separated by blank string.
                        // When it occurs we are splitting the keyword with blank string and gets multiple keywords.
                        // The multiple keywords will be used to search within OR condition.
                        // Regarding the dynamic property in criteria please check on https://stackoverflow.com/questions/11550507/build-createcriteria-in-grails-dynamically-and-in-a-dry-way
                        queryArgument.keyword.split("\\s+").each { String key ->
                            // Equivalent to SQL like expression.
                            or {
                                like("details", "%$key%")
                                like("remark", "%$key%")
                                like("formattedAddress", "%$key%")
                            }
                        }
                    } else {
                        // This makes OR as default condition if keywordsSearchCondition is not supplied.
                        // If user wants to search with OR condition for multiple keywords then we just need to wrap all of them into one OR section.
                        or {
                            // The keyword supports separated by blank string.
                            // When it occurs we are splitting the keyword with blank string and gets multiple keywords.
                            // The multiple keywords will be used to search within OR condition.
                            // Regarding the dynamic property in criteria please check on https://stackoverflow.com/questions/11550507/build-createcriteria-in-grails-dynamically-and-in-a-dry-way
                            queryArgument.keyword.split("\\s+").each { String key ->
                                // Equivalent to SQL like expression.
                                like("details", "%$key%")
                                like("remark", "%$key%")
                                like("formattedAddress", "%$key%")
                            }
                        }
                    }
                }

                if (!SharedUtil.isNullOrEmpty(queryArgument.type)) {
                    eq("type", queryArgument.type)
                }

                if (!SharedUtil.isNullOrEmpty(queryArgument.detailType)) {
                    eq("detailType", queryArgument.detailType)
                }

                if (!SharedUtil.isNullOrEmpty(queryArgument.paymentType)) {
                    eq("paymentType", queryArgument.paymentType)
                }

                if (!SharedUtil.isNullOrEmpty(queryArgument.bankCardId)) {
                    eq("bankCard.id", queryArgument.bankCardId)
                }
            }
        }
    }

    /**
     * Creates the credit card statement date range.
     * @param Date: The Date of this month statement date.
     * @return The map instance which contains keys below.
     *          statementStartDate: The start date of the statement date.
     *          statementEndDate: The end date of the statement date.
     */
    @CompileDynamic
    private Map<String, Date> createCreditCardBillStatementDateRange(Date statementDate) {
        Date statementStartDate
        Date statementEndDate

        // If the statement date is 15. That means:
        // The statement start date is 15th of last month.
        // The statement end date is 14th of this month.
        use(TimeCategory) {
            statementStartDate = statementDate - 1.month
            statementEndDate = statementDate - 1.day
        }

        return [
            statementStartDate: statementStartDate,
            statementEndDate: statementEndDate,
        ]
    }
}